Skip to content

Prevent zap.Object from panicing on nils#1501

Merged
JacobOaks merged 1 commit intouber-go:masterfrom
alshopov:master
Jun 2, 2025
Merged

Prevent zap.Object from panicing on nils#1501
JacobOaks merged 1 commit intouber-go:masterfrom
alshopov:master

Conversation

@alshopov
Copy link
Contributor

It is possible to call zap.Object with nil or nil interface This will cause panic in zap bringing down the app using it

The standard way of handling this is via nilField - compare all the constructors on pointers. As interfaces are similar to pointers - lets handle this case the same way

Add tests in zapcore for equality on zap.Object(nil) Show ReflectType does not panic on nils
Add test that zap.Object(nil) works

Update benchmarks and use such a field to show performance and allocation are not affected excessively

Refs #1500

@alshopov
Copy link
Contributor Author

This is a proposed fix for #1500

A demo on the current problem is available in Gp playground: https://go.dev/play/p/yUgOv6aEQ3t

@codecov
Copy link

codecov bot commented May 27, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.59%. Comparing base (a6afd05) to head (e7b227a).
Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1501   +/-   ##
=======================================
  Coverage   98.59%   98.59%           
=======================================
  Files          53       53           
  Lines        3562     3565    +3     
=======================================
+ Hits         3512     3515    +3     
  Misses         42       42           
  Partials        8        8           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JacobOaks
Copy link
Contributor

JacobOaks commented May 27, 2025

Since this only uses nilField for nil interfaces (i.e., when there isn't an actual MarshalLogObject method to call) I think this makes sense, but I'd like to wait for a second pair of eyes as well. Just merged a PR that should fix the lint issue in CI.

It is possible to call zap.Object with nil or nil interface
This will cause panic in zap bringing down the app using it

The standard way of handling this is via nilField - compare
all the constructors on pointers. As interfaces are similar to
pointers - lets handle this case the same way

Add tests in zapcore for equality on zap.Object(nil)
Show ReflectType does not panic on nils
Add test that zap.Object(nil) works

Update benchmarks and use such a field to show performance and
allocation are not affected excessively

Refs uber-go#1500

Signed-off-by: Alexander Shopov <ash@kambanaria.org>
Copy link

@tchung1118 tchung1118 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense as well. Just make sure to rebase before merging to re-run the lint step in CI. Thank you for the contribution!

@alshopov
Copy link
Contributor Author

alshopov commented Jun 2, 2025

@JacobOaks , @tchung1118 - rebase done, all lint checks run and green

@JacobOaks JacobOaks merged commit 07077a6 into uber-go:master Jun 2, 2025
8 checks passed
@prashantv
Copy link
Collaborator

Just a heads up that this only helps when passing in an interface type with a nil type + value, but not in the case of a struct pointer that's nil. I think the latter is likely more common than passing a nil interface.

Repro:

func TestNilObject(t *testing.T) {
        logger := zap.NewExample() // or NewProduction, or NewDevelopment
        defer logger.Sync()

        var nilObj zapcore.ObjectMarshaler
        logger.Info("nil interface works", zap.Object("nilObj", nilObj))

        var u *user
        logger.Info("nil typed value does not", zap.Object("nilUser", u))
}

type user struct {
        name string
}

func (u *user) MarshalLogObject(enc zapcore.ObjectEncoder) error {
        enc.AddString("name", u.name)
        return nil
}

output:

=== RUN   TestNilObject
{"level":"info","msg":"nil interface works","nilObj":null}
--- FAIL: TestNilObject (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
        panic: runtime error: invalid memory address or nil pointer dereference

To handle the latter case, we recover panics and convert those to "":

zap/zapcore/field.go

Lines 214 to 229 in 07077a6

func encodeStringer(key string, stringer interface{}, enc ObjectEncoder) (retErr error) {
// Try to capture panics (from nil references or otherwise) when calling
// the String() method, similar to https://golang.org/src/fmt/print.go#L540
defer func() {
if err := recover(); err != nil {
// If it's a nil pointer, just say "<nil>". The likeliest causes are a
// Stringer that fails to guard against nil or a nil pointer for a
// value receiver, and in either case, "<nil>" is a nice result.
if v := reflect.ValueOf(stringer); v.Kind() == reflect.Ptr && v.IsNil() {
enc.AddString(key, "<nil>")
return
}
retErr = fmt.Errorf("PANIC=%v", err)
}
}()

@alshopov
Copy link
Contributor Author

alshopov commented Jun 3, 2025

@prashantv

not in the case of a struct pointer that's nil. I think the latter is likely more common than passing a nil interface.
I get your point but these are different problems and need different solutions.

What I solved is the case of a missing value in the interface.

What you point out is a MarshalLogObject method with a bug. It does nil dereference without check and the code rightfully panics. A possible correct way would be like this: https://go.dev/play/p/bHDT2unMpgs

type user struct {
	name string
}

func (u *user) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	msg := "<nil>"
	if u != nil {
		msg = u.name
	}
	enc.AddString("name", msg)
	return nil
}

You do not need nil-s at all. Here is a much simpler example without using pointers at all: https://go.dev/play/p/6gmHs7HIBlk

type user struct{}

func (u user) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	panic("Catch me if you can")
}

func main() {
	logger := zap.NewExample()
	defer logger.Sync()

	var u user
	logger.Info("proper coded nil object works", zap.Object("nilUser", u))
}

I will propose a fix for this soon.

@prashantv
Copy link
Collaborator

What I solved is the case of a missing value in the interface.

To make sure we're using the same terminology, I'm using it as described here:

interface values can be thought of as a tuple of a value and a concrete type

The check added here only works when the type is nil (and when that's the case, the value is also nil).

It does not help if the type is non-nil, but the value is nil, since it will try to make a method call.

What you point out is a MarshalLogObject method with a bug. It does nil dereference without check and the code rightfully panics.

Even if MarshalLogOjbect is on a value type (where it can't do a nil dereference), you can still cause a panic by passing a nil value:
https://go.dev/play/p/YLIJxL91nrq

type user struct{}

func (u user) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	enc.AddString("foo", "bar")
	return nil
}

func main() {
	logger := zap.NewExample()
	defer logger.Sync()

	var u *user
	logger.Info("proper coded nil object works", zap.Object("nilUser", u))
}

The solution used for stringer will also help here, but I wanted to call out that the solution here helps in limited cases, and there's other nil value cases for which #1500 is not solved.

@alshopov
Copy link
Contributor Author

alshopov commented Jun 8, 2025

I think I see what you mean now. The previous example did not show that.
In your last example: since the method set of a pointer to a type includes the methods of a type, a pointer to an ObjectMarshaler is accepted as an argument to zap.Object
When the encoder tries to invoke the method (for example) it results in a nil dereference.
I can try to implement something similar. That would also cover the badly implemented ObjectMarshaler case.

TimoBehrendt pushed a commit to TimoBehrendt/tracebasedlogsampler that referenced this pull request Feb 9, 2026
This PR contains the following updates:

| Package | Type | Update | Change | Pending |
|---|---|---|---|---|
| [go.opentelemetry.io/collector/component](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v1.45.0` → `v1.50.0` | `v1.51.0` |
| [go.opentelemetry.io/collector/component/componenttest](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v0.139.0` → `v0.144.0` | `v0.145.0` |
| [go.opentelemetry.io/collector/confmap](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v1.45.0` → `v1.50.0` | `v1.51.0` |
| [go.opentelemetry.io/collector/consumer](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v1.45.0` → `v1.50.0` | `v1.51.0` |
| [go.opentelemetry.io/collector/consumer/consumertest](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v0.139.0` → `v0.144.0` | `v0.145.0` |
| [go.opentelemetry.io/collector/pdata](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v1.45.0` → `v1.50.0` | `v1.51.0` |
| [go.opentelemetry.io/collector/processor](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v1.45.0` → `v1.50.0` | `v1.51.0` |
| [go.opentelemetry.io/collector/processor/processortest](https://github.com/open-telemetry/opentelemetry-collector) | require | minor | `v0.139.0` → `v0.144.0` | `v0.145.0` |
| [go.uber.org/zap](https://github.com/uber-go/zap) | require | patch | `v1.27.0` → `v1.27.1` |  |

---

### Release Notes

<details>
<summary>open-telemetry/opentelemetry-collector (go.opentelemetry.io/collector/component)</summary>

### [`v1.50.0`](https://github.com/open-telemetry/opentelemetry-collector/blob/HEAD/CHANGELOG.md#v1500v01440)

##### 🛑 Breaking changes 🛑

- `pkg/exporterhelper`: Change verbosity level for otelcol\_exporter\_queue\_batch\_send\_size metric to detailed. ([#&#8203;14278](open-telemetry/opentelemetry-collector#14278))
- `pkg/service`: Remove deprecated `telemetry.disableHighCardinalityMetrics` feature gate. ([#&#8203;14373](open-telemetry/opentelemetry-collector#14373))
- `pkg/service`: Remove deprecated `service.noopTracerProvider` feature gate. ([#&#8203;14374](open-telemetry/opentelemetry-collector#14374))

##### 🚩 Deprecations 🚩

- `exporter/otlp_grpc`: Rename `otlp` exporter to `otlp_grpc` exporter and add deprecated alias `otlp`. ([#&#8203;14403](open-telemetry/opentelemetry-collector#14403))
- `exporter/otlp_http`: Rename `otlphttp` exporter to `otlp_http` exporter and add deprecated alias `otlphttp`. ([#&#8203;14396](open-telemetry/opentelemetry-collector#14396))

##### 💡 Enhancements 💡

- `cmd/builder`: Avoid duplicate CLI error logging in generated collector binaries by relying on cobra's error handling. ([#&#8203;14317](open-telemetry/opentelemetry-collector#14317))

- `cmd/mdatagen`: Add the ability to disable attributes at the metric level and re-aggregate data points based off of these new dimensions ([#&#8203;10726](open-telemetry/opentelemetry-collector#10726))

- `cmd/mdatagen`: Add optional `display_name` and `description` fields to metadata.yaml for human-readable component names ([#&#8203;14114](open-telemetry/opentelemetry-collector#14114))
  The `display_name` field allows components to specify a human-readable name in metadata.yaml.
  When provided, this name is used as the title in generated README files.
  The `description` field allows components to include a brief description in generated README files.

- `cmd/mdatagen`: Validate stability level for entities ([#&#8203;14425](open-telemetry/opentelemetry-collector#14425))

- `pkg/xexporterhelper`: Reenable batching for profiles ([#&#8203;14313](open-telemetry/opentelemetry-collector#14313))

- `receiver/nop`: add profiles signal support ([#&#8203;14253](open-telemetry/opentelemetry-collector#14253))

##### 🧰 Bug fixes 🧰

- `pkg/exporterhelper`: Fix reference count bug in partition batcher ([#&#8203;14444](open-telemetry/opentelemetry-collector#14444))

<!-- previous-version -->

### [`v1.49.0`](https://github.com/open-telemetry/opentelemetry-collector/blob/HEAD/CHANGELOG.md#v1490v01430)

##### 💡 Enhancements 💡

- `all`: Update semconv import to 1.38.0 ([#&#8203;14305](open-telemetry/opentelemetry-collector#14305))
- `exporter/nop`: Add profiles support to nop exporter ([#&#8203;14331](open-telemetry/opentelemetry-collector#14331))
- `pkg/pdata`: Optimize the size and pointer bytes for pdata structs ([#&#8203;14339](open-telemetry/opentelemetry-collector#14339))
- `pkg/pdata`: Avoid using interfaces/oneof like style for optional fields ([#&#8203;14333](open-telemetry/opentelemetry-collector#14333))

<!-- previous-version -->

### [`v1.48.0`](https://github.com/open-telemetry/opentelemetry-collector/blob/HEAD/CHANGELOG.md#v1480v01420)

##### 💡 Enhancements 💡

- `exporter/debug`: Add logging of dropped attributes, events, and links counts in detailed verbosity ([#&#8203;14202](open-telemetry/opentelemetry-collector#14202))

- `extension/memory_limiter`: The memorylimiter extension can be used as an HTTP/GRPC middleware. ([#&#8203;14081](open-telemetry/opentelemetry-collector#14081))

- `pkg/config/configgrpc`: Statically validate gRPC endpoint ([#&#8203;10451](open-telemetry/opentelemetry-collector#10451))
  This validation was already done in the OTLP exporter. It will now be applied to any gRPC client.

- `pkg/service`: Add support to disabling adding resource attributes as zap fields in internal logging ([#&#8203;13869](open-telemetry/opentelemetry-collector#13869))
  Note that this does not affect logs exported through OTLP.

<!-- previous-version -->

### [`v1.47.0`](https://github.com/open-telemetry/opentelemetry-collector/blob/HEAD/CHANGELOG.md#v1470v01410)

##### 🛑 Breaking changes 🛑

- `pkg/config/confighttp`: Use configoptional.Optional for confighttp.ClientConfig.Cookies field ([#&#8203;14021](open-telemetry/opentelemetry-collector#14021))

##### 💡 Enhancements 💡

- `pkg/config/confighttp`: Setting `compression_algorithms` to an empty list now disables automatic decompression, ignoring Content-Encoding ([#&#8203;14131](open-telemetry/opentelemetry-collector#14131))
- `pkg/service`: Update semantic conventions from internal telemetry to v1.37.0 ([#&#8203;14232](open-telemetry/opentelemetry-collector#14232))
- `pkg/xscraper`: Implement xscraper for Profiles. ([#&#8203;13915](open-telemetry/opentelemetry-collector#13915))

##### 🧰 Bug fixes 🧰

- `pkg/config/configoptional`: Ensure that configoptional.None values resulting from unmarshaling are equivalent to configoptional.Optional zero value. ([#&#8203;14218](open-telemetry/opentelemetry-collector#14218))

<!-- previous-version -->

### [`v1.46.0`](https://github.com/open-telemetry/opentelemetry-collector/blob/HEAD/CHANGELOG.md#v1460v01400)

##### 💡 Enhancements 💡

- `cmd/mdatagen`: `metadata.yaml` now supports an optional `entities` section to organize resource attributes into logical entities with identity and description attributes ([#&#8203;14051](open-telemetry/opentelemetry-collector#14051))
  When entities are defined, mdatagen generates `AssociateWith{EntityType}()` methods on ResourceBuilder
  that associate resources with entity types using the entity refs API. The entities section is backward
  compatible - existing metadata.yaml files without entities continue to work as before.

- `cmd/mdatagen`: Add semconv reference for metrics ([#&#8203;13920](open-telemetry/opentelemetry-collector#13920))

- `connector/forward`: Add support for Profiles to Profiles ([#&#8203;14092](open-telemetry/opentelemetry-collector#14092))

- `exporter/debug`: Disable sending queue by default ([#&#8203;14138](open-telemetry/opentelemetry-collector#14138))
  The recently added sending queue configuration in Debug exporter was enabled by default and had a problematic default size of 1.
  This change disables the sending queue by default.
  Users can enable and configure the sending queue if needed.

- `pkg/config/configoptional`: Mark `configoptional.AddEnabledField` as beta ([#&#8203;14021](open-telemetry/opentelemetry-collector#14021))

- `pkg/otelcol`: This feature has been improved and tested; secure-by-default redacts configopaque values ([#&#8203;12369](open-telemetry/opentelemetry-collector#12369))

##### 🧰 Bug fixes 🧰

- `all`: Ensure service service.instance.id is the same for all the signals when it is autogenerated. ([#&#8203;14140](open-telemetry/opentelemetry-collector#14140))

<!-- previous-version -->

</details>

<details>
<summary>uber-go/zap (go.uber.org/zap)</summary>

### [`v1.27.1`](https://github.com/uber-go/zap/releases/tag/v1.27.1)

[Compare Source](uber-go/zap@v1.27.0...v1.27.1)

Enhancements:

- [#&#8203;1501][]: prevent `Object` from panicking on nils
- [#&#8203;1511][]: Fix a race condition in `WithLazy`.

Thanks to [@&#8203;rabbbit](https://github.com/rabbbit), [@&#8203;alshopov](https://github.com/alshopov), [@&#8203;jquirke](https://github.com/jquirke), [@&#8203;arukiidou](https://github.com/arukiidou) for their contributions to this release.

[#&#8203;1501]: uber-go/zap#1501

[#&#8203;1511]: uber-go/zap#1511

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4xMC41IiwidXBkYXRlZEluVmVyIjoiNDIuOTUuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Reviewed-on: https://gitea.t000-n.de/t.behrendt/tracebasedlogsampler/pulls/25
Reviewed-by: t.behrendt <t.behrendt@noreply.localhost>
Co-authored-by: Renovate Bot <renovate@t00n.de>
Co-committed-by: Renovate Bot <renovate@t00n.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants