|
51 | 51 | * cluster_undo_path_resolve |
52 | 52 | * |
53 | 53 | * Pure path builder (no I/O, no errors apart from buffer overflow). |
| 54 | + * |
| 55 | + * Hardening v1.0.4 P1-1 (directory naming separation): |
| 56 | + * - The owner_instance VALUE in headers / WAL payloads is |
| 57 | + * (cluster_node_id + 1), so 0 stays the "unallocated" sentinel. |
| 58 | + * - The DIRECTORY NAME on disk uses cluster_node_id directly |
| 59 | + * (= owner_instance - 1) so that single-node default |
| 60 | + * (cluster_node_id = 0) lays out at pg_undo/instance_0/... |
| 61 | + * matching the initdb seed segment (also at instance_0/). |
| 62 | + * Otherwise allocator segments would land at instance_1/ and |
| 63 | + * split from the seed, breaking the per-instance subdir |
| 64 | + * invariant. |
| 65 | + * |
| 66 | + * Caller MUST pass owner_instance in [1, UNDO_OWNER_INSTANCE_MAX]; |
| 67 | + * the assert catches sentinel-0 misuse. |
54 | 68 | */ |
55 | 69 | int |
56 | | -cluster_undo_path_resolve(uint8 instance, uint32 segment_id, char *buf, size_t buf_size) |
| 70 | +cluster_undo_path_resolve(uint8 owner_instance, uint32 segment_id, char *buf, size_t buf_size) |
57 | 71 | { |
58 | 72 | int ret; |
59 | 73 |
|
60 | 74 | if (buf == NULL || buf_size == 0) |
61 | 75 | return -1; |
| 76 | + Assert(owner_instance >= 1 && owner_instance <= UNDO_OWNER_INSTANCE_MAX); |
62 | 77 |
|
63 | | - ret = snprintf(buf, buf_size, "%s/pg_undo/instance_%u/seg_%u.dat", DataDir, (unsigned)instance, |
64 | | - (unsigned)segment_id); |
| 78 | + /* directory uses cluster_node_id (= owner_instance - 1) */ |
| 79 | + ret = snprintf(buf, buf_size, "%s/pg_undo/instance_%u/seg_%u.dat", DataDir, |
| 80 | + (unsigned)(owner_instance - 1), (unsigned)segment_id); |
65 | 81 | if (ret < 0 || (size_t)ret >= buf_size) |
66 | 82 | return -1; |
67 | 83 | return 0; |
@@ -111,16 +127,21 @@ open_or_create_segment(const char *path, bool *out_created) |
111 | 127 | * rejects it, but if a future caller does, mkdir() handles it. |
112 | 128 | */ |
113 | 129 | static void |
114 | | -ensure_instance_subdir(uint8 instance) |
| 130 | +ensure_instance_subdir(uint8 owner_instance) |
115 | 131 | { |
116 | 132 | char path[MAXPGPATH]; |
117 | 133 | int ret; |
118 | 134 |
|
119 | | - ret = snprintf(path, sizeof(path), "%s/pg_undo/instance_%u", DataDir, (unsigned)instance); |
| 135 | + Assert(owner_instance >= 1 && owner_instance <= UNDO_OWNER_INSTANCE_MAX); |
| 136 | + |
| 137 | + /* directory uses cluster_node_id (= owner_instance - 1); see |
| 138 | + * cluster_undo_path_resolve docstring. */ |
| 139 | + ret = snprintf(path, sizeof(path), "%s/pg_undo/instance_%u", DataDir, |
| 140 | + (unsigned)(owner_instance - 1)); |
120 | 141 | if (ret < 0 || (size_t)ret >= sizeof(path)) |
121 | | - ereport(ERROR, |
122 | | - (errcode(ERRCODE_NAME_TOO_LONG), |
123 | | - errmsg("undo instance subdir path too long: instance=%u", (unsigned)instance))); |
| 142 | + ereport(ERROR, (errcode(ERRCODE_NAME_TOO_LONG), |
| 143 | + errmsg("undo instance subdir path too long: owner_instance=%u", |
| 144 | + (unsigned)owner_instance))); |
124 | 145 |
|
125 | 146 | if (mkdir(path, S_IRWXU) < 0 && errno != EEXIST) |
126 | 147 | ereport(ERROR, (errcode_for_file_access(), |
@@ -171,24 +192,30 @@ cluster_undo_segment_allocate(uint32 segment_id, uint8 owner_instance) |
171 | 192 | ensure_instance_subdir(owner_instance); |
172 | 193 |
|
173 | 194 | fd = open_or_create_segment(path, &created); |
| 195 | + (void)created; /* tracked for logging/observability; ftruncate is unconditional */ |
174 | 196 |
|
175 | 197 | cluster_undo_segment_make_header_bytes(segment_id, owner_instance, page.data); |
176 | 198 |
|
177 | 199 | /* |
178 | | - * Extend file to UNDO_SEGMENT_SIZE_BYTES on first creation. ftruncate |
179 | | - * gives a sparse file (tail bytes read as zero); the allocator path |
180 | | - * writes actual undo records lazily later. |
| 200 | + * Hardening v1.0.4 P1-2: unconditional ftruncate. |
| 201 | + * |
| 202 | + * v1.0.3 only ftruncate'd when created=true, but a crash between |
| 203 | + * O_CREAT and ftruncate leaves a partial / zero-size orphan file. |
| 204 | + * The next allocate call sees the existing file, so created=false, |
| 205 | + * and the file would never reach UNDO_SEGMENT_SIZE_BYTES -- segment |
| 206 | + * tail accesses would EOF. ftruncate is idempotent (shrink-to-same |
| 207 | + * and extend-to-target are both no-ops when size already matches), |
| 208 | + * so unconditional is safe and self-healing. Mirrors the redo |
| 209 | + * handler's 6-step idempotent pattern (cluster_undo_xlog.c). |
181 | 210 | */ |
182 | | - if (created) { |
183 | | - if (ftruncate(fd, (off_t)UNDO_SEGMENT_SIZE_BYTES) != 0) { |
184 | | - int save_errno = errno; |
185 | | - |
186 | | - close(fd); |
187 | | - errno = save_errno; |
188 | | - ereport(ERROR, (errcode_for_file_access(), |
189 | | - errmsg("could not extend undo segment file \"%s\" to %d bytes: %m", |
190 | | - path, UNDO_SEGMENT_SIZE_BYTES))); |
191 | | - } |
| 211 | + if (ftruncate(fd, (off_t)UNDO_SEGMENT_SIZE_BYTES) != 0) { |
| 212 | + int save_errno = errno; |
| 213 | + |
| 214 | + close(fd); |
| 215 | + errno = save_errno; |
| 216 | + ereport(ERROR, (errcode_for_file_access(), |
| 217 | + errmsg("could not extend undo segment file \"%s\" to %d bytes: %m", path, |
| 218 | + UNDO_SEGMENT_SIZE_BYTES))); |
192 | 219 | } |
193 | 220 |
|
194 | 221 | written = pg_pwrite(fd, page.data, BLCKSZ, 0); |
@@ -216,6 +243,21 @@ cluster_undo_segment_allocate(uint32 segment_id, uint8 owner_instance) |
216 | 243 | ereport(ERROR, (errcode_for_file_access(), |
217 | 244 | errmsg("could not close undo segment file \"%s\": %m", path))); |
218 | 245 |
|
| 246 | + /* |
| 247 | + * Hardening v1.0.4 P1-2: fsync parent directory after file creation / |
| 248 | + * truncate. Required for the create case so the dirent is durable; |
| 249 | + * harmless for the already-exists case. Mirrors redo handler. |
| 250 | + */ |
| 251 | + { |
| 252 | + char dir[MAXPGPATH]; |
| 253 | + int dret; |
| 254 | + |
| 255 | + dret = snprintf(dir, sizeof(dir), "%s/pg_undo/instance_%u", DataDir, |
| 256 | + (unsigned)(owner_instance - 1)); |
| 257 | + if (dret >= 0 && (size_t)dret < sizeof(dir)) |
| 258 | + fsync_fname(dir, true); |
| 259 | + } |
| 260 | + |
219 | 261 | /* |
220 | 262 | * Emit WAL record so crash recovery can recreate the header (the |
221 | 263 | * segment file itself is created earlier in this function; a crash |
|
0 commit comments