1 | // SPDX-License-Identifier: GPL-2.0 |
2 | |
3 | #define dev_fmt(fmt) "mtdoops-pstore: " fmt |
4 | |
5 | #include <linux/kernel.h> |
6 | #include <linux/module.h> |
7 | #include <linux/pstore_blk.h> |
8 | #include <linux/mtd/mtd.h> |
9 | #include <linux/bitops.h> |
10 | #include <linux/slab.h> |
11 | |
12 | static struct mtdpstore_context { |
13 | int index; |
14 | struct pstore_blk_config info; |
15 | struct pstore_device_info dev; |
16 | struct mtd_info *mtd; |
17 | unsigned long *rmmap; /* removed bit map */ |
18 | unsigned long *usedmap; /* used bit map */ |
19 | /* |
20 | * used for panic write |
21 | * As there are no block_isbad for panic case, we should keep this |
22 | * status before panic to ensure panic_write not failed. |
23 | */ |
24 | unsigned long *badmap; /* bad block bit map */ |
25 | } oops_cxt; |
26 | |
27 | static int mtdpstore_block_isbad(struct mtdpstore_context *cxt, loff_t off) |
28 | { |
29 | int ret; |
30 | struct mtd_info *mtd = cxt->mtd; |
31 | u64 blknum; |
32 | |
33 | off = ALIGN_DOWN(off, mtd->erasesize); |
34 | blknum = div_u64(dividend: off, divisor: mtd->erasesize); |
35 | |
36 | if (test_bit(blknum, cxt->badmap)) |
37 | return true; |
38 | ret = mtd_block_isbad(mtd, ofs: off); |
39 | if (ret < 0) { |
40 | dev_err(&mtd->dev, "mtd_block_isbad failed, aborting\n" ); |
41 | return ret; |
42 | } else if (ret > 0) { |
43 | set_bit(nr: blknum, addr: cxt->badmap); |
44 | return true; |
45 | } |
46 | return false; |
47 | } |
48 | |
49 | static inline int mtdpstore_panic_block_isbad(struct mtdpstore_context *cxt, |
50 | loff_t off) |
51 | { |
52 | struct mtd_info *mtd = cxt->mtd; |
53 | u64 blknum; |
54 | |
55 | off = ALIGN_DOWN(off, mtd->erasesize); |
56 | blknum = div_u64(dividend: off, divisor: mtd->erasesize); |
57 | return test_bit(blknum, cxt->badmap); |
58 | } |
59 | |
60 | static inline void mtdpstore_mark_used(struct mtdpstore_context *cxt, |
61 | loff_t off) |
62 | { |
63 | struct mtd_info *mtd = cxt->mtd; |
64 | u64 zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
65 | |
66 | dev_dbg(&mtd->dev, "mark zone %llu used\n" , zonenum); |
67 | set_bit(nr: zonenum, addr: cxt->usedmap); |
68 | } |
69 | |
70 | static inline void mtdpstore_mark_unused(struct mtdpstore_context *cxt, |
71 | loff_t off) |
72 | { |
73 | struct mtd_info *mtd = cxt->mtd; |
74 | u64 zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
75 | |
76 | dev_dbg(&mtd->dev, "mark zone %llu unused\n" , zonenum); |
77 | clear_bit(nr: zonenum, addr: cxt->usedmap); |
78 | } |
79 | |
80 | static inline void mtdpstore_block_mark_unused(struct mtdpstore_context *cxt, |
81 | loff_t off) |
82 | { |
83 | struct mtd_info *mtd = cxt->mtd; |
84 | u32 zonecnt = mtd->erasesize / cxt->info.kmsg_size; |
85 | u64 zonenum; |
86 | |
87 | off = ALIGN_DOWN(off, mtd->erasesize); |
88 | zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
89 | while (zonecnt > 0) { |
90 | dev_dbg(&mtd->dev, "mark zone %llu unused\n" , zonenum); |
91 | clear_bit(nr: zonenum, addr: cxt->usedmap); |
92 | zonenum++; |
93 | zonecnt--; |
94 | } |
95 | } |
96 | |
97 | static inline int mtdpstore_is_used(struct mtdpstore_context *cxt, loff_t off) |
98 | { |
99 | u64 zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
100 | u64 blknum = div_u64(dividend: off, divisor: cxt->mtd->erasesize); |
101 | |
102 | if (test_bit(blknum, cxt->badmap)) |
103 | return true; |
104 | return test_bit(zonenum, cxt->usedmap); |
105 | } |
106 | |
107 | static int mtdpstore_block_is_used(struct mtdpstore_context *cxt, |
108 | loff_t off) |
109 | { |
110 | struct mtd_info *mtd = cxt->mtd; |
111 | u32 zonecnt = mtd->erasesize / cxt->info.kmsg_size; |
112 | u64 zonenum; |
113 | |
114 | off = ALIGN_DOWN(off, mtd->erasesize); |
115 | zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
116 | while (zonecnt > 0) { |
117 | if (test_bit(zonenum, cxt->usedmap)) |
118 | return true; |
119 | zonenum++; |
120 | zonecnt--; |
121 | } |
122 | return false; |
123 | } |
124 | |
125 | static int mtdpstore_is_empty(struct mtdpstore_context *cxt, char *buf, |
126 | size_t size) |
127 | { |
128 | struct mtd_info *mtd = cxt->mtd; |
129 | size_t sz; |
130 | int i; |
131 | |
132 | sz = min_t(uint32_t, size, mtd->writesize / 4); |
133 | for (i = 0; i < sz; i++) { |
134 | if (buf[i] != (char)0xFF) |
135 | return false; |
136 | } |
137 | return true; |
138 | } |
139 | |
140 | static void mtdpstore_mark_removed(struct mtdpstore_context *cxt, loff_t off) |
141 | { |
142 | struct mtd_info *mtd = cxt->mtd; |
143 | u64 zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
144 | |
145 | dev_dbg(&mtd->dev, "mark zone %llu removed\n" , zonenum); |
146 | set_bit(nr: zonenum, addr: cxt->rmmap); |
147 | } |
148 | |
149 | static void mtdpstore_block_clear_removed(struct mtdpstore_context *cxt, |
150 | loff_t off) |
151 | { |
152 | struct mtd_info *mtd = cxt->mtd; |
153 | u32 zonecnt = mtd->erasesize / cxt->info.kmsg_size; |
154 | u64 zonenum; |
155 | |
156 | off = ALIGN_DOWN(off, mtd->erasesize); |
157 | zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
158 | while (zonecnt > 0) { |
159 | clear_bit(nr: zonenum, addr: cxt->rmmap); |
160 | zonenum++; |
161 | zonecnt--; |
162 | } |
163 | } |
164 | |
165 | static int mtdpstore_block_is_removed(struct mtdpstore_context *cxt, |
166 | loff_t off) |
167 | { |
168 | struct mtd_info *mtd = cxt->mtd; |
169 | u32 zonecnt = mtd->erasesize / cxt->info.kmsg_size; |
170 | u64 zonenum; |
171 | |
172 | off = ALIGN_DOWN(off, mtd->erasesize); |
173 | zonenum = div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
174 | while (zonecnt > 0) { |
175 | if (test_bit(zonenum, cxt->rmmap)) |
176 | return true; |
177 | zonenum++; |
178 | zonecnt--; |
179 | } |
180 | return false; |
181 | } |
182 | |
183 | static int mtdpstore_erase_do(struct mtdpstore_context *cxt, loff_t off) |
184 | { |
185 | struct mtd_info *mtd = cxt->mtd; |
186 | struct erase_info erase; |
187 | int ret; |
188 | |
189 | off = ALIGN_DOWN(off, cxt->mtd->erasesize); |
190 | dev_dbg(&mtd->dev, "try to erase off 0x%llx\n" , off); |
191 | erase.len = cxt->mtd->erasesize; |
192 | erase.addr = off; |
193 | ret = mtd_erase(mtd: cxt->mtd, instr: &erase); |
194 | if (!ret) |
195 | mtdpstore_block_clear_removed(cxt, off); |
196 | else |
197 | dev_err(&mtd->dev, "erase of region [0x%llx, 0x%llx] on \"%s\" failed\n" , |
198 | (unsigned long long)erase.addr, |
199 | (unsigned long long)erase.len, cxt->info.device); |
200 | return ret; |
201 | } |
202 | |
203 | /* |
204 | * called while removing file |
205 | * |
206 | * Avoiding over erasing, do erase block only when the whole block is unused. |
207 | * If the block contains valid log, do erase lazily on flush_removed() when |
208 | * unregister. |
209 | */ |
210 | static ssize_t mtdpstore_erase(size_t size, loff_t off) |
211 | { |
212 | struct mtdpstore_context *cxt = &oops_cxt; |
213 | |
214 | if (mtdpstore_block_isbad(cxt, off)) |
215 | return -EIO; |
216 | |
217 | mtdpstore_mark_unused(cxt, off); |
218 | |
219 | /* If the block still has valid data, mtdpstore do erase lazily */ |
220 | if (likely(mtdpstore_block_is_used(cxt, off))) { |
221 | mtdpstore_mark_removed(cxt, off); |
222 | return 0; |
223 | } |
224 | |
225 | /* all zones are unused, erase it */ |
226 | return mtdpstore_erase_do(cxt, off); |
227 | } |
228 | |
229 | /* |
230 | * What is security for mtdpstore? |
231 | * As there is no erase for panic case, we should ensure at least one zone |
232 | * is writable. Otherwise, panic write will fail. |
233 | * If zone is used, write operation will return -ENOMSG, which means that |
234 | * pstore/blk will try one by one until gets an empty zone. So, it is not |
235 | * needed to ensure the next zone is empty, but at least one. |
236 | */ |
237 | static int mtdpstore_security(struct mtdpstore_context *cxt, loff_t off) |
238 | { |
239 | int ret = 0, i; |
240 | struct mtd_info *mtd = cxt->mtd; |
241 | u32 zonenum = (u32)div_u64(dividend: off, divisor: cxt->info.kmsg_size); |
242 | u32 zonecnt = (u32)div_u64(dividend: cxt->mtd->size, divisor: cxt->info.kmsg_size); |
243 | u32 blkcnt = (u32)div_u64(dividend: cxt->mtd->size, divisor: cxt->mtd->erasesize); |
244 | u32 erasesize = cxt->mtd->erasesize; |
245 | |
246 | for (i = 0; i < zonecnt; i++) { |
247 | u32 num = (zonenum + i) % zonecnt; |
248 | |
249 | /* found empty zone */ |
250 | if (!test_bit(num, cxt->usedmap)) |
251 | return 0; |
252 | } |
253 | |
254 | /* If there is no any empty zone, we have no way but to do erase */ |
255 | while (blkcnt--) { |
256 | div64_u64_rem(dividend: off + erasesize, divisor: cxt->mtd->size, remainder: (u64 *)&off); |
257 | |
258 | if (mtdpstore_block_isbad(cxt, off)) |
259 | continue; |
260 | |
261 | ret = mtdpstore_erase_do(cxt, off); |
262 | if (!ret) { |
263 | mtdpstore_block_mark_unused(cxt, off); |
264 | break; |
265 | } |
266 | } |
267 | |
268 | if (ret) |
269 | dev_err(&mtd->dev, "all blocks bad!\n" ); |
270 | dev_dbg(&mtd->dev, "end security\n" ); |
271 | return ret; |
272 | } |
273 | |
274 | static ssize_t mtdpstore_write(const char *buf, size_t size, loff_t off) |
275 | { |
276 | struct mtdpstore_context *cxt = &oops_cxt; |
277 | struct mtd_info *mtd = cxt->mtd; |
278 | size_t retlen; |
279 | int ret; |
280 | |
281 | if (mtdpstore_block_isbad(cxt, off)) |
282 | return -ENOMSG; |
283 | |
284 | /* zone is used, please try next one */ |
285 | if (mtdpstore_is_used(cxt, off)) |
286 | return -ENOMSG; |
287 | |
288 | dev_dbg(&mtd->dev, "try to write off 0x%llx size %zu\n" , off, size); |
289 | ret = mtd_write(mtd: cxt->mtd, to: off, len: size, retlen: &retlen, buf: (u_char *)buf); |
290 | if (ret < 0 || retlen != size) { |
291 | dev_err(&mtd->dev, "write failure at %lld (%zu of %zu written), err %d\n" , |
292 | off, retlen, size, ret); |
293 | return -EIO; |
294 | } |
295 | mtdpstore_mark_used(cxt, off); |
296 | |
297 | mtdpstore_security(cxt, off); |
298 | return retlen; |
299 | } |
300 | |
301 | static inline bool mtdpstore_is_io_error(int ret) |
302 | { |
303 | return ret < 0 && !mtd_is_bitflip(err: ret) && !mtd_is_eccerr(err: ret); |
304 | } |
305 | |
306 | /* |
307 | * All zones will be read as pstore/blk will read zone one by one when do |
308 | * recover. |
309 | */ |
310 | static ssize_t mtdpstore_read(char *buf, size_t size, loff_t off) |
311 | { |
312 | struct mtdpstore_context *cxt = &oops_cxt; |
313 | struct mtd_info *mtd = cxt->mtd; |
314 | size_t retlen, done; |
315 | int ret; |
316 | |
317 | if (mtdpstore_block_isbad(cxt, off)) |
318 | return -ENOMSG; |
319 | |
320 | dev_dbg(&mtd->dev, "try to read off 0x%llx size %zu\n" , off, size); |
321 | for (done = 0, retlen = 0; done < size; done += retlen) { |
322 | retlen = 0; |
323 | |
324 | ret = mtd_read(mtd: cxt->mtd, from: off + done, len: size - done, retlen: &retlen, |
325 | buf: (u_char *)buf + done); |
326 | if (mtdpstore_is_io_error(ret)) { |
327 | dev_err(&mtd->dev, "read failure at %lld (%zu of %zu read), err %d\n" , |
328 | off + done, retlen, size - done, ret); |
329 | /* the zone may be broken, try next one */ |
330 | return -ENOMSG; |
331 | } |
332 | |
333 | /* |
334 | * ECC error. The impact on log data is so small. Maybe we can |
335 | * still read it and try to understand. So mtdpstore just hands |
336 | * over what it gets and user can judge whether the data is |
337 | * valid or not. |
338 | */ |
339 | if (mtd_is_eccerr(err: ret)) { |
340 | dev_err(&mtd->dev, "ecc error at %lld (%zu of %zu read), err %d\n" , |
341 | off + done, retlen, size - done, ret); |
342 | /* driver may not set retlen when ecc error */ |
343 | retlen = retlen == 0 ? size - done : retlen; |
344 | } |
345 | } |
346 | |
347 | if (mtdpstore_is_empty(cxt, buf, size)) |
348 | mtdpstore_mark_unused(cxt, off); |
349 | else |
350 | mtdpstore_mark_used(cxt, off); |
351 | |
352 | mtdpstore_security(cxt, off); |
353 | return retlen; |
354 | } |
355 | |
356 | static ssize_t mtdpstore_panic_write(const char *buf, size_t size, loff_t off) |
357 | { |
358 | struct mtdpstore_context *cxt = &oops_cxt; |
359 | struct mtd_info *mtd = cxt->mtd; |
360 | size_t retlen; |
361 | int ret; |
362 | |
363 | if (mtdpstore_panic_block_isbad(cxt, off)) |
364 | return -ENOMSG; |
365 | |
366 | /* zone is used, please try next one */ |
367 | if (mtdpstore_is_used(cxt, off)) |
368 | return -ENOMSG; |
369 | |
370 | ret = mtd_panic_write(mtd: cxt->mtd, to: off, len: size, retlen: &retlen, buf: (u_char *)buf); |
371 | if (ret < 0 || size != retlen) { |
372 | dev_err(&mtd->dev, "panic write failure at %lld (%zu of %zu read), err %d\n" , |
373 | off, retlen, size, ret); |
374 | return -EIO; |
375 | } |
376 | mtdpstore_mark_used(cxt, off); |
377 | |
378 | return retlen; |
379 | } |
380 | |
381 | static void mtdpstore_notify_add(struct mtd_info *mtd) |
382 | { |
383 | int ret; |
384 | struct mtdpstore_context *cxt = &oops_cxt; |
385 | struct pstore_blk_config *info = &cxt->info; |
386 | unsigned long longcnt; |
387 | |
388 | if (!strcmp(mtd->name, info->device)) |
389 | cxt->index = mtd->index; |
390 | |
391 | if (mtd->index != cxt->index || cxt->index < 0) |
392 | return; |
393 | |
394 | dev_dbg(&mtd->dev, "found matching MTD device %s\n" , mtd->name); |
395 | |
396 | if (mtd->size < info->kmsg_size * 2) { |
397 | dev_err(&mtd->dev, "MTD partition %d not big enough\n" , |
398 | mtd->index); |
399 | return; |
400 | } |
401 | /* |
402 | * kmsg_size must be aligned to 4096 Bytes, which is limited by |
403 | * psblk. The default value of kmsg_size is 64KB. If kmsg_size |
404 | * is larger than erasesize, some errors will occur since mtdpstore |
405 | * is designed on it. |
406 | */ |
407 | if (mtd->erasesize < info->kmsg_size) { |
408 | dev_err(&mtd->dev, "eraseblock size of MTD partition %d too small\n" , |
409 | mtd->index); |
410 | return; |
411 | } |
412 | if (unlikely(info->kmsg_size % mtd->writesize)) { |
413 | dev_err(&mtd->dev, "record size %lu KB must align to write size %d KB\n" , |
414 | info->kmsg_size / 1024, |
415 | mtd->writesize / 1024); |
416 | return; |
417 | } |
418 | |
419 | longcnt = BITS_TO_LONGS(div_u64(mtd->size, info->kmsg_size)); |
420 | cxt->rmmap = kcalloc(n: longcnt, size: sizeof(long), GFP_KERNEL); |
421 | cxt->usedmap = kcalloc(n: longcnt, size: sizeof(long), GFP_KERNEL); |
422 | |
423 | longcnt = BITS_TO_LONGS(div_u64(mtd->size, mtd->erasesize)); |
424 | cxt->badmap = kcalloc(n: longcnt, size: sizeof(long), GFP_KERNEL); |
425 | |
426 | /* just support dmesg right now */ |
427 | cxt->dev.flags = PSTORE_FLAGS_DMESG; |
428 | cxt->dev.zone.read = mtdpstore_read; |
429 | cxt->dev.zone.write = mtdpstore_write; |
430 | cxt->dev.zone.erase = mtdpstore_erase; |
431 | cxt->dev.zone.panic_write = mtdpstore_panic_write; |
432 | cxt->dev.zone.total_size = mtd->size; |
433 | |
434 | ret = register_pstore_device(dev: &cxt->dev); |
435 | if (ret) { |
436 | dev_err(&mtd->dev, "mtd%d register to psblk failed\n" , |
437 | mtd->index); |
438 | return; |
439 | } |
440 | cxt->mtd = mtd; |
441 | dev_info(&mtd->dev, "Attached to MTD device %d\n" , mtd->index); |
442 | } |
443 | |
444 | static int mtdpstore_flush_removed_do(struct mtdpstore_context *cxt, |
445 | loff_t off, size_t size) |
446 | { |
447 | struct mtd_info *mtd = cxt->mtd; |
448 | u_char *buf; |
449 | int ret; |
450 | size_t retlen; |
451 | struct erase_info erase; |
452 | |
453 | buf = kmalloc(size: mtd->erasesize, GFP_KERNEL); |
454 | if (!buf) |
455 | return -ENOMEM; |
456 | |
457 | /* 1st. read to cache */ |
458 | ret = mtd_read(mtd, from: off, len: mtd->erasesize, retlen: &retlen, buf); |
459 | if (mtdpstore_is_io_error(ret)) |
460 | goto free; |
461 | |
462 | /* 2nd. erase block */ |
463 | erase.len = mtd->erasesize; |
464 | erase.addr = off; |
465 | ret = mtd_erase(mtd, instr: &erase); |
466 | if (ret) |
467 | goto free; |
468 | |
469 | /* 3rd. write back */ |
470 | while (size) { |
471 | unsigned int zonesize = cxt->info.kmsg_size; |
472 | |
473 | /* there is valid data on block, write back */ |
474 | if (mtdpstore_is_used(cxt, off)) { |
475 | ret = mtd_write(mtd, to: off, len: zonesize, retlen: &retlen, buf); |
476 | if (ret) |
477 | dev_err(&mtd->dev, "write failure at %lld (%zu of %u written), err %d\n" , |
478 | off, retlen, zonesize, ret); |
479 | } |
480 | |
481 | off += zonesize; |
482 | size -= min_t(unsigned int, zonesize, size); |
483 | } |
484 | |
485 | free: |
486 | kfree(objp: buf); |
487 | return ret; |
488 | } |
489 | |
490 | /* |
491 | * What does mtdpstore_flush_removed() do? |
492 | * When user remove any log file on pstore filesystem, mtdpstore should do |
493 | * something to ensure log file removed. If the whole block is no longer used, |
494 | * it's nice to erase the block. However if the block still contains valid log, |
495 | * what mtdpstore can do is to erase and write the valid log back. |
496 | */ |
497 | static int mtdpstore_flush_removed(struct mtdpstore_context *cxt) |
498 | { |
499 | struct mtd_info *mtd = cxt->mtd; |
500 | int ret; |
501 | loff_t off; |
502 | u32 blkcnt = (u32)div_u64(dividend: mtd->size, divisor: mtd->erasesize); |
503 | |
504 | for (off = 0; blkcnt > 0; blkcnt--, off += mtd->erasesize) { |
505 | ret = mtdpstore_block_isbad(cxt, off); |
506 | if (ret) |
507 | continue; |
508 | |
509 | ret = mtdpstore_block_is_removed(cxt, off); |
510 | if (!ret) |
511 | continue; |
512 | |
513 | ret = mtdpstore_flush_removed_do(cxt, off, size: mtd->erasesize); |
514 | if (ret) |
515 | return ret; |
516 | } |
517 | return 0; |
518 | } |
519 | |
520 | static void mtdpstore_notify_remove(struct mtd_info *mtd) |
521 | { |
522 | struct mtdpstore_context *cxt = &oops_cxt; |
523 | |
524 | if (mtd->index != cxt->index || cxt->index < 0) |
525 | return; |
526 | |
527 | mtdpstore_flush_removed(cxt); |
528 | |
529 | unregister_pstore_device(dev: &cxt->dev); |
530 | kfree(objp: cxt->badmap); |
531 | kfree(objp: cxt->usedmap); |
532 | kfree(objp: cxt->rmmap); |
533 | cxt->mtd = NULL; |
534 | cxt->index = -1; |
535 | } |
536 | |
537 | static struct mtd_notifier mtdpstore_notifier = { |
538 | .add = mtdpstore_notify_add, |
539 | .remove = mtdpstore_notify_remove, |
540 | }; |
541 | |
542 | static int __init mtdpstore_init(void) |
543 | { |
544 | int ret; |
545 | struct mtdpstore_context *cxt = &oops_cxt; |
546 | struct pstore_blk_config *info = &cxt->info; |
547 | |
548 | ret = pstore_blk_get_config(info); |
549 | if (unlikely(ret)) |
550 | return ret; |
551 | |
552 | if (strlen(info->device) == 0) { |
553 | pr_err("mtd device must be supplied (device name is empty)\n" ); |
554 | return -EINVAL; |
555 | } |
556 | if (!info->kmsg_size) { |
557 | pr_err("no backend enabled (kmsg_size is 0)\n" ); |
558 | return -EINVAL; |
559 | } |
560 | |
561 | /* Setup the MTD device to use */ |
562 | ret = kstrtoint(s: (char *)info->device, base: 0, res: &cxt->index); |
563 | if (ret) |
564 | cxt->index = -1; |
565 | |
566 | register_mtd_user(new: &mtdpstore_notifier); |
567 | return 0; |
568 | } |
569 | module_init(mtdpstore_init); |
570 | |
571 | static void __exit mtdpstore_exit(void) |
572 | { |
573 | unregister_mtd_user(old: &mtdpstore_notifier); |
574 | } |
575 | module_exit(mtdpstore_exit); |
576 | |
577 | MODULE_LICENSE("GPL" ); |
578 | MODULE_AUTHOR("WeiXiong Liao <liaoweixiong@allwinnertech.com>" ); |
579 | MODULE_DESCRIPTION("MTD backend for pstore/blk" ); |
580 | |